If your embedded system must communicate with past,
present, and future technology, abstraction can take you there. It's
always a good way to create portable software.
Stealing silently through darkened labs, lying in ambush behind
stacks of design documents, the specter of multiplatform development
is ever eager to snare another unwary development team. Its jaws are
multifarious code changes and its claws eternal technical support.
Its greatest weapon is the lack of development strategies that
address multiplatform software development.
Confronting the Hydra
In the past, developers were not afflicted with the burdens of
writing code that executed on a wide variety of platforms. A company
that markets I/O cards, for example, might supply example programs
or device drivers for a select group of real-time operating systems.
But usually these were distinct software components that were
maintained as separate products. Until relatively recently, entire
embedded applications were not expected to execute on different
hardware platforms, under different operating systems.
At a growing number of companies, developers are being asked to
design embedded systems that can run on more than one hardware
platform, and sometimes under more than one operating system. The
driving force behind this trend is the decreasing time between
consecutive generations of a product. Not only has the duration of
development cycles decreased, the time between development cycles
has decreased as well!
A highly competitive marketplace demands that the next generation
of a product be designed even as the previous one heads out the
door. Developers find they must leverage the software from the
previous generation to accelerate the next generation's time to
market. But advances in technology frustrate this goal. System
designers select different devices, subsystems, and CPUs to improve
the next generation product. As a result, software developers often
face a porting issue that they never anticipated.
The economic downturn has slowed the pace of development, but it
has also brought into play other forces that promote multiplatform
efforts. Companies now also strive to cut costs by applying existing
designs to a wider range of applications. A board that was the heart
of a media gateway may now also become part of a wireless
base-station.
When software is not designed to anticipate these possibilities,
the result can be more costly than launching a new design. Major
overhauls to the code, constant support calls to the original
software developers, duplication of testing effort, and major
redesigns all combine to confound the original goals of cost saving
and accelerated time to market.
Figure 1: Components of a multiplatform software
system
Abstract art
Is software development an art or a science? Significantly,
Donald Knuth's books were titled The Art of Computer Programming. If
software development is an art, then its highest expression is the
creative way in which developers solve daunting technical problems,
such as multiplatform development. One of the most creative ways to
approach multiplatform development is through the strategic use of
abstraction.
Abstraction distills an application to its root concepts while
isolating it from the details of the system implementation. The key
is to compartmentalize the system such that the application's
algorithms are decoupled from the implementation environment. A
camera pointing system, for example, should operate on velocities,
coordinates, and distances to target. It is irrelevant whether a
radar device or an optical tracking subsystem generated the ranging
information. Does the algorithm really need to know that some of the
rate information was produced by a Hall effect sensor?
This approach is effective over a wide variety of applications.
Just as the mechanical components of a system can be abstracted, so
can communications interfaces, CPU resources, RTOS facilities, and
storage devices. Deft use of abstraction simplifies the application
into a compact, reusable code base. It is a powerful tool when
applied to the problems of multiplatform development.
The following sections provide a guideline to follow when
choosing abstractions to aid multiplatform development. As always,
creativity, elegance, simplicity, and pragmatism should guide the
selection of abstractions.
OS abstraction
Never call OS functions directly. Divorce the application from
the underlying RTOS by encapsulating operating system functions in
an OS abstraction library. This permits developers to migrate the
application to different operating systems simply by porting the OS
abstraction layer. The application code remains intact.
Testing and quality assurance performed previously on the
application are not wasted because the application code remains
unchanged. The "hardening" that the software acquired in the field
will carry over to the new platform and accelerate the transition to
the new environment. Also, when bugs are discovered, the OS
abstraction library becomes the starting point for the debugging
effort.
The OS abstraction library should export function prototypes and
guarantee specific behaviors for them, regardless of the underlying
operating system. For example, many operating systems have
semaphores that are safe to take more than once from the same task
context. Other operating systems will block the caller if a
recursive semaphore take is attempted. If the OS abstraction library
exports a function, OS_SemTake(), then it must proscribe a
standard behavior for the function independent from the underlying
operating system. In the case of a recursive semaphore take
function, developers might have to implement the recursive behavior
themselves. The implementation for some operating systems might
involve comparing the ID of the task that last acquired the
semaphore to the ID of the task attempting to take it. If they are
the same, the function should increment a counter for that semaphore
and not make the OS's semaphore take system call. The corresponding
give function decrements the counter each time it is called until
the counter reaches zero. At that time, the OS semaphore give
function is called to relinquish the semaphore.
Write wrappers even for functions you would expect to behave
similarly on all operating systems. This protects the application
from idiosyncrasies and even bugs in the underlying operating system
implementations. "Standard" functions often aren't, and "well-known"
functions could make fortunes writing tell-all books about their
secret lives.
The socket library function, select(), provides a good
example of why encapsulation is so valuable. The types of devices
that can be "select-ed" vary considerably from operating system to
operating system. Some allow only sockets to be selected, while
others permit sockets, pipes, and message queues. Abstracting the
underlying implementations protects the application against an
unwanted, and often messy, redesign for another operating system. In
one case, an operating system failed to implement the timeout
feature of select() properly. Fortunately, the work-around
could be confined to the abstraction library. Otherwise, significant
architectural changes to the application may have been required.
Logical interfaces
Convert physical interfaces to logical ones. As an example,
consider a system that has an engineering bus to which peripherals
are attached. Create a set of functions that access the bus, such as
EngBus_SendTo() and EngBus_ReadFrom(), that
effectively hides the implementation of the bus from the
application. Whether the bus implementation is PCI, VME, 1553B, or a
serial port, it is separated from the application logic.
This approach works well for networked applications. If an
embedded client engages in a session with a networked server over
Ethernet-TCP/IP, abstract the concept of session and partition it
from the IP connection. Allow the application to invoke functions
that reflect the logical session, such as Session_Open(),
Session_Close(), Session_Send(),
Session_Receive(), and Session_BlockForSessionData().
This allows the application to migrate to a platform where the
physical interface may be a paltry serial port. The concept of the
session has not changed, only the medium over which the session is
conducted. If abstracted judiciously, the application can be
embedded in different types of equipment with a variety of physical
interfaces.
The same approach applies to applications that might be
distributed among several processors in later generations of the
product. Abstracting interprocess communications (IPC) mechanisms
allows the software subsystems of the application to move freely
from one processor to another. By partitioning the software in this
manner, you can scale the application from an entry-level product to
the behemoth model by adding processors to the system board. The
converse occurs when bridging generations of products. Software
required to translate between different generations of hardware can
be removed in later generations of the product when the older
technology has been completely removed.
Commanding the system is abstracted in the same manner. Whether
the commands are generated by command line interface (CLI), infrared
link, or an embedded SNMP agent does not matter. The mechanism by
which the commands enter the system should be divided from the set
of functions invoked to execute the commands.
The same is true of event logging and general I/O. By distilling
the logical connections from the physical, the application can be
rewired into dozens of different platforms. The advantages of this
approach are a reusable code base that can be leveraged between
different platforms as well as a consistent interface across
different product lines from the same manufacturer.
Isolate protocols
Separate the protocol implementation from both the transport
medium and the system-specific details of the application. In our
camera-pointing example, the pointing algorithm was isolated from
the sources of its input data (the physical sensors). The protocol
implementation can be isolated from the transport medium in exactly
the same way. For example, implement a packet-based protocol as a
code library that accepts packets as input and produces response
packets as output. For connection-oriented protocols that maintain
state, the library can also accept and modify data structures that
preserve state information related to the connection.
This code organization provides the freedom to embed the protocol
in any number of devices at a later date. It also permits the
testing of the protocol to occur in isolation, which is an
invaluable capability. The implementation can be debugged prior to
the appearance of hardware and used later as a static test-bed to
simulate failures in the field.
It is important not to place application-specific knowledge into
the protocol implementation. For example, an application-level
protocol implementation that produces AAL5 packets cannot easily be
placed into a device that uses IP. Similarly, an implementation that
accepts a TCP socket descriptor, to which it sends responses, cannot
easily be employed across a proprietary backplane.
Use system services
System services are software components that provide a service to
the application level software (see "At Your
Service," Steven Stolper, April 2001, p. 124). Developers employ
them to abstract the details of the hardware platform into a
standard set of capabilities used by the application. They can be
used to manage nonvolatile storage, provide highly accurate timing
through hardware support, and manage processor resources. Similar
services can also be used to manage relays, switches, or other
peripheral hardware manipulated at the application level.
System services can also provide software services that do not
depend on the existence of specific physical hardware. Services such
as interprocess communications, software health verification, event
logging, and time-stamping can be rendered independently of
specialized hardware.
This streamlines multiplatform development by leveraging the
existing, well-tested application software. The application migrates
to different platforms by porting the lower level services on which
the application resides. It also allows the application code-base to
be easily ported to more capable hardware when it becomes available
simply by porting the underlying services.
Build a framework
Another way to increase the portability of the application is to
place all of the platform-specific initialization into one module.
When the system boots, the root task invokes a platform-specific
framework module to initialize and configure the system-specific
hardware. Once the platform-specific initialization is complete, the
framework starts the application. The platform-specific code
executes outside of the application, so the application can run
under any number of frameworks.
When developing applications that might be retargeted to other
platforms, it is wise to consider what other development
environments might be used to build the software. Companies with
heterogeneous environments that consist of both Unix and Windows
computers need to keep in mind differences between the systems. Some
Windows environments will accept either slashes or backslashes in
file path names. Unix environments require forward slashes. Unix
environments are also case sensitive. When specifying header files,
the capitalization must match the way the file appears in the file
system. Also consider how Unix and Windows editors deal with special
characters that may be embedded in the application code such as
tabs, end-of-line, and carriage return. It could be disastrous to
have to modify the source of a critical application after all of the
testing has been completed.
Abstraction is a powerful tool for multiplatform development.
Wisely employed, it can transform the potential for multifarious
code changes, insidious bugs, and eternal technical support into a
triumph of engineering -and even art!
Steven Stolper is a software engineering manager at
Broadcom's Carrier-Access Business Unit. Prior to Broadcom, he
helped develop embedded IP-over-satellite networks. Steve also
designed flight software for NASA planetary spacecraft including the
Mars Pathfinder Lander and Galileo Orbiter. His e-mail address is marsguy@ix.netcom.com.
Acknowledgements
Erik Guntvedt, Subbarao Mungara, and Randy Pan contributed
valuable ideas and opinions that greatly influenced this article.
Forth is a niche programming language originally designed
for real-time control of telescopes. Over the years, it evolved into
an ANSI-standard language. While not widely used anymore, it's still
worth a look.
Forth is a niche programming language originally designed for
real-time control of telescopes. As programmers from other fields
discovered Forth, a grassroots effort emerged to mold it into an
ANSI-standard language.
Programmers who've used Forth describe the language as being like
a room without walls. Some thrive on such freedom, while others are
uncomfortable with it. Since Forth is a type-less language, the
compiler can do little checking for you before you run your program.
As a result, the most common failure scenario is a system crash.
Forth is used mostly to test and debug hardware and bring up
systems. Only about one in 50 embedded developers report using Forth
regularly. Interestingly, some UNIX workstations boot a small Forth
interpreter before the rest of the operating system. This
environment provides some basic programming capabilities right out
of ROM, and a small Forth bootloader stored there enables the
operating system to be manually or automatically loaded from a disk
drive or over a network and then run.
The Forth
estate
Forth is a language with a simple syntax and many keywords. This
is in contrast to Algol-style languages (such as Pascal and C/C++),
which have a complex syntax and few keywords. If you're completely
new to Forth, try to forget everything you know about programming
languages as you read on.
Forth programs are made of many small procedures. Forth is
compiled, yet has no compiler in the traditional sense. Essentially,
it's a population of subroutines and an interpreter. The subroutines
are called words. (In this article, words will appear in
UPPER-CASE.) The dictionary is a data structure that associates the
compiled words with their string names. The interpreter can invoke
words that perform compilation actions, thereby extending the
dictionary in the middle of a program. Figure 1 shows a flow chart
of a Forth interpreter. The interpreter evaluates white
space-delimited strings taken from an input stream, such as a
console or file, usually in one pass.
Figure 1: A Forth interpreter's
flow chart
Word games
You write a Forth program by defining new words, and run it by
executing the top-level word. Forth manipulates data on a parameter
stack that is separate from the call stack. (There are no
registers.) Although static variables can be defined, words
generally pop their parameters from the parameter stack, and push
their results onto it. For example, the built-in word + pops the top
two values, adds them, and pushes the sum back onto the stack.
Bitwise AND operates similarly. The word < pops two values,
compares them, and pushes the result (0 or -1). So a Forth
programmer would code (2+3)*(4+5) as 2 3 + 4 5 + *, in reverse
polish notation (RPN).
The Forth standard specifies a boolean result as all 0's or all
1's, which is 0 or -1 in twos complement arithmetic. This allows you
to mix arithmetic and boolean operations, for example << 7
AND. The compiler allows any kind of type mixing, as Forth is
typeless.
With most data kept on the parameter stack, there's little need
to track variable names or addresses, and temporary storage is
automatic. Several built-in words manipulate the stack by rotating,
removing, copying, or displaying items from various stack positions:
SWAP swaps the top two items, DROP removes the top stack item, and
OVER copies the second stack item to the top of the stack, thereby
increasing the stack size by one.
Words that manipulate character strings generally require a
pointer as the second item on the stack and the string length on the
top. Branching and looping words also use the stack. IF pops and
tests the value on top of the stack. If the value is non-zero, the
next word executes. Otherwise, control passes to the word following
the ELSE, if present. BEGIN starts an indefinite loop and the
corresponding END pops and tests the top stack value, looping back
to BEGIN if it's zero. DO/LOOP pairs repeat until an index (passed
on the stack) increments to or beyond a limit (also passed on the
stack). An important difference is that the index and limit are
copied to the call stack-to avoid cluttering the parameter stack.
Listing 1: A Forth program and its stack
: BIGGEST OVER OVER < IF SWAP THEN DROP ; 5 9 BIGGEST .
Listing 1 shows the definition and testing of a new word,
BIGGEST. The word : switches to compiler mode and begins the
definition of the new word. OVER OVER duplicates the values to be
compared while maintaining their order. The word < compares them,
popping the two values copied by OVER OVER and pushing the result of
the comparison, which is subsequently popped and tested by IF. If
the comparison results in a -1, control passes to SWAP, which swaps
the two values. Either way, the smaller value is now on top, ready
to be removed by DROP. The word ; terminates the definition and
takes the interpreter out of compile mode.
Once defined, we can use a new word immediately. 5 9 BIGGEST
pushes 5, then 9, then removes the smaller value. The word . prints
the value on top of the stack (in this case 9), and the stack is
again empty. The state of the stack as the program executes is shown
below the code.
Go Forth and
prosper
Forth has earned a reputation for being a write-only language. A
typical Forth program defines and uses thousands of new words. In
the absence of good naming conventions and comments, this can be a
big maintenance headache. On the other hand, there's no reason Forth
programs can't be useful and well documented. esp
Brad Eckert holds a BS in physics from Shippensburg
University. He has been a designer of both hardware and software for
embedded systems for about 15 years. Brad wrote and maintains a free
Forth-based framework for extensible firmware. His e-mail address is
brad@tinyboot.com.
Don Rowe is a consultant specializing in embedded
controllers. He has over 25 years of experience with digital and
analog design, software testing, and reverse engineering. Contact
him at don@canzonatech.com.
Further
Reading
http://www.forth.org/
Conklin,
Edward and Elizabeth Rather. Forth Programmer's Handbook.
Hawthorne, CA: Forth Inc., 1998.
Return to the
September 2002 Table of Contents